...loading
2024-12-24
프론트엔드의 꽃과 같은 성능 개선을 시도해봅니다. 이번 프로젝트에서 nextJs를 채택한 이유는 모노레포로 풀스택을 개발하는 것도 있지만, SSR의 장점을 통해 성능의 이점을 가져가는 목적도 있습니다. 현재 기본적인 CRUD도 개발되었고 프로젝트의 구조도 어느정도 잡혀있기에 성능 개선을 시도합니다.
성능 개선의 첫 번째 목표는 메인 페이지의 캐러셀입니다. 현재 캐러셀 이미지의 로딩 지연이 발생합니다. 이에 따른 깜빡임이 육안으로 느껴질 정도이기에 개선을 시도하겠습니다.
// 최신 Dev posts 캐러셀 "use client"; ... export default function DevPosts() { const [cardData, setCardData] = useState<PostDataProps[]>([]); useEffect(() => { const fetchData = async () => { try { const response = await fetch("/api/post/get-all-posts"); if (!response.ok) { throw new Error("Failed to fetch data"); } const result = await response.json(); setCardData(sortRecentDevPosts(result)); } catch (error) { console.error("Error fetching data:", error); } }; fetchData(); }, []); return ( <div className={styles["wrapper"]}> <header className={styles["header"]}> <h1 className={styles["title"]}>Dev Posts</h1> <NavButton text="More Posts" size="small" link="/posts/development" /> </header> <main className={styles["content-wrapper"]}> <Carousel data={cardData} /> </main> </div> ); }
기존의 코드는 클라이언트 컴포넌트로 작성되었습니다. useEffect를 통해 data fetching을 진행하고 있습니다. nextJs를 사용하여 서버 컴포넌트로 전환할 수 있는 부분임에도 불구하고, 클라이언트 컴포넌트로 작성되었기에 성능의 이점을 잘 못살리고 있습니다.
위 컴포넌트의 경우 클라이언트 컴포넌트로 작성될 특별한 이유가 없습니다. 클라이언트에서 관리될 상태나 이벤트가 필요하지 않기 때문에 서버 컴포넌트로 바꿔줍니다. 기대할 수 있는 이점은 초기 로딩 속도의 개선입니다.
... export default async function DevPosts() { const fetchResult = await fetchPostList(); const data = sortRecentDevPosts(fetchResult); return ( <Suspense fallback={<p>...loading</p>}> <div className={styles["wrapper"]}> <header className={styles["header"]}> <h1 className={styles["title"]}>Dev posts</h1> <NavButton text="More Posts" size="small" link="/posts/development" /> </header> <main className={styles["content-wrapper"]}> <Carousel data={data} /> </main> </div> </Suspense> ); }
서버 컴포넌트로 개선한 코드입니다. 서버 컴포넌트 적용은 간단합니다. 기존의 함수 컴포넌트에 async를 적용하면 서버 컴포넌트가 됩니다. 그리고 미리 작성한 포스팅 리스트를 가져오는 fetch함수를 통해 데이터를 서버측에서 바로 받아 작업을 처리합니다. 이렇게 서버측에서 바로 데이터가 포함된 HTML을 생성하여 클라이언트에 전달되므로 성능의 이점을 기대할 수 있습니다. 추가로 SEO의 이점도 가져갈 수 있겠죠? 일석이조입니다.
서버 컴포넌트의 성능 차이를 확인해보겠습니다. 오른쪽 Dev posts가 서버 컴포넌트를 적용한 캐러셀인데 육안으로 봐도 깜빡임 현상이 확연히 줄어든게 확인됩니다. 역시나 SSR이 적용되니 로딩 속도가 꽤나 빠릅니다. 만족스러운 결과입니다. 가장 눈에 거슬리는 현상을 해결했군요.
앞선 작업 이후 Lighthouse를 통해 성능 테스트를 해봤습니다. 생각만큼 양호한 점수가 나오지는 않더군요.. 90점 이상은 나올거라 생각했는데 80점 밖에 나오지 않습니다.
Lighthouse에서 분석한 문제는 다음과 같습니다. 역시나 메인 페이지의 고화질 이미지가 문제인 것 같네요.. 배경 이미지 때문에 FCP와 LGP 측정치 모두 좋지 못하게 나오고 있습니다. 그리고 두 번째 개선점은 폰트입니다. 폰트가 클라이언트에 완전히 적용되기 이전까지 약간의 딜레이가 발생합니다. 따라서 이 부분도 같이 개선을 시도합니다.
import Image from "next/image"; ... // 기존 코드 <Image src={wallpaper} alt="wallpaper" fill/> // 개선된 코드 <Image src={wallpaper} alt="wallpaper" quality={60} fill priority />
이미지를 개선하는 방법은 의외로 간단합니다. nextJs의 Image태그에서 지원하는 속성을 사용하면 최적화에 큰 도움이 됩니다. 우선 priority 속성을 추가하여 해당 배경이미지가 가장 먼저 뷰포트에 로드될 수 있도록 설정을 해줍니다. 그리고 새로 알게된 Image태그의 quality라는 속성을 적용해봅니다. 디폴트값으로 75%가 설정되어 있는데 60%로 낮추어 기존 이미지 크기의 부담을 줄여줍니다. 실제로 성능 측정 결과에서 이미지 크기를 소폭 줄일 수 있었습니다. (60% 이하는 애정하는 프로젝트의 퀄리티를 위해 양보할 수 없습니다..)
이정도까지 해야할까 싶지만 폰트의 로딩도 최적화를 시켜줍니다. 현재 지정한 폰트 디자인이 적용되어 클라이언트에 표시되기까지 약간의 딜레이가 발생합니다. 이 경우에 font-display : swap
을 적용하면 폰트가 로드되기 전까지 시스템 폰트를 적용하여 글자를 보여줄 수 있습니다. 적용 코드는 간단합니다. 아래와 같습니다.
@font-face { font-family: 'CustomFont'; src: url('/fonts/custom-font.woff2') format('woff2'); font-display: swap; }
Lighthouse가 분석해준 문제들을 개선했습니다. 이제 어떠한 점수가 나오는지 다시 테스트해봅니다.
결과는 97점.. 나름 만족스럽군요 ㅎㅎ 이전에 비해 약 21% 정도의 성능 개선을 달성했습니다. 그래도 100점이 아닌건 좀 아쉽습니다. 아무래도 100점을 달성하기 위해서는 배경화면의 이미지 퀄리티를 상당하게 줄이거나 디자인을 바꿔야하지 않을까 싶습니다. 하지만 기존의 디자인을 유지한 상태로 97점이라는 좋은 성능을 달성했습니다.
Comments